> Polymarket CLOB Deep Dive

Evergreen
planted Mar 14, 2026tended May 6, 2026
#research#web3#defi#prediction-markets#polymarket#clob#polygon

Polymarket CLOB Deep Dive

Deep technical reference for Polymarket's Central Limit Order Book β€” the unified order book model, token structure (neg-risk vs standard), API endpoints, order lifecycle, py-clob-client SDK usage, and resolution / redemption mechanics.

Unified order book

Polymarket uses a unified book β€” there are no separate order books for YES and NO. The core principle:

BUY YES at X = SELL NO at (1 βˆ’ X)

When two complementary BUY orders arrive (e.g., BUY YES at $0.60 and BUY NO at $0.40), the matching engine recognizes they sum to $1.00 and executes both by splitting $1.00 USDC.e from the collateral pool into 1 YES token + 1 NO token. Tokens are created on-demand through this split, not pre-minted.

The reverse (two SELL orders at complementary prices) merges tokens back into $1.00 USDC.e. This means every BUY YES order simultaneously appears as a SELL NO on the other side, effectively doubling liquidity.

Token structure

Standard (non-neg-risk) markets

A single market with two outcomes. Example: "Purdue vs Nebraska"

  • outcomes = ["Purdue Boilermakers", "Nebraska Cornhuskers"]
  • clobTokenIds = [purdue_token, nebraska_token]
  • One order book where buying Purdue = selling Nebraska
  • Token prices are complementary: if Purdue is at $0.90, Nebraska is at $0.10

Neg-risk markets

Multiple separate markets under one event, each with YES/NO outcomes. Example: "La Liga: Real Madrid vs Elche"

  • Market 1: "Will Real Madrid win?" β†’ clobTokenIds = [YES, NO]
  • Market 2: "Will Elche win?" β†’ same structure
  • Market 3: "Will it draw?" β†’ same structure

Each market has its own independent order book. The Neg Risk Adapter contract converts between them: holding 1 NO token in any market can be converted into 1 YES token in every other market.

clobTokenIds semantics

| Market type | clobTokenIds[0] | clobTokenIds[1] | |-------------|-------------------|-------------------| | Neg-risk | YES token | NO token | | Non-neg-risk 2-way | First team's win token | Second team's win token |

Bot bug I hit: Buying clobTokenIds[1] as "NO" is correct for neg-risk but wrong for non-neg-risk β€” it buys the second team's token instead. Must check event.negRisk and handle accordingly.

Identifying market type

  • event.negRisk = true in Gamma API
  • All markets have outcomes = ["Yes", "No"] β†’ neg-risk
  • Two named outcomes β†’ non-neg-risk

CLOB API endpoints

GET /midpoint

Returns (best_bid + best_ask) / 2. No auth required.

GET https://clob.polymarket.com/midpoint?token_id=TOKEN_ID
β†’ {"mid": "0.45"}

If spread > $0.10, the Polymarket UI shows last-trade price instead. Returns 404 if the orderbook is closed.

GET /price

Returns top-of-book price for a specific side.

GET https://clob.polymarket.com/price?token_id=TOKEN_ID&side=BUY
β†’ {"price": 0.45}
  • side=BUY β†’ best ask (what you'd pay)
  • side=SELL β†’ best bid (what you'd receive)

GET /book

Full order book with all price levels.

GET https://clob.polymarket.com/book?token_id=TOKEN_ID

Returns bids (highest first), asks (lowest first), plus neg_risk, tick_size, last_trade_price, min_order_size.

Endpoint comparison

| Endpoint | Returns | Use case | |----------|---------|----------| | /midpoint | (best_bid + best_ask) / 2 | Quick probability estimate | | /price?side=BUY | Best ask | Actual execution price for buying | | /price?side=SELL | Best bid | Actual execution price for selling | | /book | Full depth | Liquidity analysis |

Order lifecycle

Placement

  1. Create the order locally (sign with EIP-712).
  2. Post to the CLOB operator.
  3. Receive initial status: live, matched, delayed, or unmatched.

Initial statuses

| Status | Meaning | |--------|---------| | live | Resting on the book | | matched | Immediately matched | | delayed | Sports markets β€” 3-second hold before matching | | unmatched | Was marketable but delay expired without match |

Settlement

MATCHED β†’ MINED β†’ CONFIRMED (success), or β†’ RETRYING β†’ FAILED.

Order types

| Type | Behavior | |------|----------| | GTC | Good-til-cancelled, rests indefinitely | | GTD | Good-til-date, expires at timestamp | | FOK | Fill-or-kill, must fill entirely and immediately | | FAK | Fill-and-kill, fills what's available, cancels rest |

Sports 3-second delay

Sports markets have a mandatory secondsDelay (typically 3 s) on marketable orders. Prevents front-running during live events. Visible in Gamma market data as "secondsDelay": 3.

py-clob-client SDK

Order creation

One-step (simple):

from py_clob_client.clob_types import OrderArgs, PartialCreateOrderOptions
from py_clob_client.order_builder.constants import BUY, SELL

resp = client.create_and_post_order(
    OrderArgs(token_id="...", price=0.50, size=10, side=BUY),
    options=PartialCreateOrderOptions(tick_size="0.01", neg_risk=True),
)

Two-step (more control):

signed = client.create_order(order_args, options)
resp = client.post_order(signed, OrderType.GTC)

Critical: the neg_risk parameter affects how the order is signed β€” different exchange contracts for neg-risk vs standard. Wrong value β†’ "not enough balance / allowance" error. Default is True (neg-risk), which fails silently for non-neg-risk markets.

Balance and allowance

from py_clob_client.clob_types import BalanceAllowanceParams, AssetType

# Sync for conditional tokens
client.update_balance_allowance(
    BalanceAllowanceParams(asset_type=AssetType.CONDITIONAL, token_id="...")
)

# Sync for USDC collateral
client.update_balance_allowance(
    BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)
)

Key methods

| Method | Purpose | |--------|---------| | get_midpoint(token_id) | Midpoint price | | get_price(token_id, side) | Best bid / ask | | get_order_book(token_id) | Full depth | | get_neg_risk(token_id) | Check market type | | get_tick_size(token_id) | Required tick size | | create_and_post_order(args, options) | Create + sign + post | | cancel(order_id) | Cancel by ID | | get_order(order_id) | Check status | | update_balance_allowance(params) | Sync allowance with CLOB |

Resolution and redemption

Resolution flow

  1. Market end-condition met.
  2. Anyone proposes resolution with a $750 USDC.e bond.
  3. 2-hour challenge window β€” if undisputed, accepted.
  4. If disputed: second proposal + another challenge window (4–5 days).
  5. If disputed twice: UMA token-holder vote (5–6 days).

Payout vector: YES wins = [1, 0], NO wins = [0, 1].

Redemption

The Polymarket UI auto-redeems winning positions, but programmatic wallets must redeem manually.

On-chain redemption:

ctf.redeemPositions(
    collateralToken=USDC_E,          # 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174
    parentCollectionId=bytes32(0),
    conditionId=CONDITION_ID,
    indexSets=[1, 2],
)

Burns the entire token balance for that condition. Only works after resolution.

Neg-risk redemption gap: for neg-risk markets, standard CTF redemption doesn't release USDC directly. The Neg Risk Adapter wraps tokens differently. Workaround: place exit SELL orders at $0.999 before resolution. py-clob-client has no redeem method (see Issue #139).

Contract addresses (Polygon)

| Contract | Address | |----------|---------| | USDC.e | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 | | CTF (ERC1155) | 0x4D97DCd97eC945f40cF65F87097ACe5EA0476045 | | CTF Exchange | 0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E | | NegRisk CTF Exchange | 0xC5d563A36AE78145C45a50134d48A1215220f80a | | NegRisk Adapter | 0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296 |

Required: setApprovalForAll on CTF contract for all three operator addresses, then sync via update_balance_allowance().

Open questions

  • Auto-redemption for programmatic wallets β€” likely only works through UI / proxy wallet, not raw EOAs.
  • Native USDC migration β€” Circle replacing USDC.e with native USDC may change collateral address.
  • Exit orders as redemption workaround β€” SELL at $0.999 before resolution fails with allowance errors. Need update_balance_allowance per token right after BUY.
  • /book endpoint staleness β€” known stale-data bug reported Feb 2026.

Connection points

  • This is the protocol-level reference behind polycli β€” most of the CLI's surface area (poly markets, poly orderbook, poly address) maps directly to the endpoints documented here.
  • Pairs with LS-LMSR Deep Dive β€” that one covers the AMM math (Hanson's LMSR + Othman/Sandholm's liquidity-sensitive extension) used by older prediction-market venues; this one covers the order-book model Polymarket actually uses today.

Sources